package com.sksamuel.avro4s

import com.sksamuel.avro4s.SchemaUpdate.{FullSchemaUpdate, NoUpdate}
import org.apache.avro.{Schema, SchemaBuilder}

import scala.language.experimental.macros
import scala.language.implicitConversions
import scala.reflect.ClassTag
import scala.reflect.runtime.universe._

/**
  * A [[SchemaFor]] generates an Avro Schema for a Scala or Java type.
  *
  * For example, a String SchemaFor could return an instance of Schema.Type.STRING
  * or Schema.Type.FIXED depending on the type required for Strings.
  */
trait SchemaFor[T] extends Serializable {
  self =>

  def schema: Schema
  def fieldMapper: FieldMapper

  /**
    * Creates a SchemaFor[U] by applying a function Schema => Schema
    * to the schema generated by this instance.
    */
  def map[U](fn: Schema => Schema): SchemaFor[U] = SchemaFor[U](fn(schema), fieldMapper)

  /**
    * Changes the type of this SchemaFor to the desired type `U` without any other modifications.
    * @tparam U new type for SchemaFor.
    */
  def forType[U]: SchemaFor[U] = map[U](identity)

  /**
    * produces a SchemaFor that is guaranteed to be resolved and ready to be used.
    *
    * This is necessary for properly setting up SchemaFor instances for recursive types.
    */
  def resolveSchemaFor(): SchemaFor[T] = resolveSchemaFor(DefinitionEnvironment.empty, NoUpdate)

  /**
    * For advanced use only to properly setup SchemaFor instances for recursive types.
    *
    * Resolves the SchemaFor with the provided environment, and (potentially) pushes down overrides from annotations on
    * sealed traits to case classes, or from annotations on parameters to types.
    *
    * @param env definition environment containing already defined record schemas
    * @param update schema changes to apply
    */
  def resolveSchemaFor(env: DefinitionEnvironment[SchemaFor], update: SchemaUpdate): SchemaFor[T] =
    (self, update) match {
      case (resolvable: ResolvableSchemaFor[T], _) => resolvable.schemaFor(env, update)
      case (_, FullSchemaUpdate(sf))               => sf.forType
      case _                                       => self
    }
}

/**
  * A SchemaFor that needs to be resolved before it is usable. Resolution is needed to properly setup SchemaFor instances
  * for recursive types.
  *
  * If this instance is used without resolution, it falls back to use an adhoc-resolved instance and delegates all
  * operations to it. This involves a performance penalty of lazy val access that can be avoided by
  * calling [[SchemaFor.resolveSchemaFor]] and using that.
  *
  * For examples on how to define custom ResolvableSchemaFor instances, see the Readme and RecursiveSchemaTest.
  *
  * @tparam T type this schema is for (primitive type, case class, sealed trait, or enum e.g.).
  */
trait ResolvableSchemaFor[T] extends SchemaFor[T] {

  /**
    * Creates a SchemaFor instance (and applies schema changes given) or returns an already existing value from the
    * given definition environment.
    *
    * @param env definition environment to use
    * @param update schema update to apply
    * @return either an already existing value from env or a new created instance.
    */
  def schemaFor(env: DefinitionEnvironment[SchemaFor], update: SchemaUpdate): SchemaFor[T]

  lazy val adhocInstance = schemaFor(DefinitionEnvironment.empty, NoUpdate)

  def schema = adhocInstance.schema
  def fieldMapper: FieldMapper = adhocInstance.fieldMapper
}

case class ScalePrecision(scale: Int, precision: Int)

object ScalePrecision {
  implicit val default = ScalePrecision(2, 8)
}

trait EnumSchemaFor {

  import scala.collection.JavaConverters._

  protected def addDefault[E](default: E)(schema: Schema): Schema =
    SchemaBuilder
      .enumeration(schema.getName)
      .namespace(schema.getNamespace)
      .defaultSymbol(default.toString)
      .symbols(schema.getEnumSymbols.asScala.toList: _*)
}

object JavaEnumSchemaFor extends EnumSchemaFor {

  def apply[E <: Enum[_]](default: E)(implicit tag: ClassTag[E]): SchemaFor[E] =
    SchemaFor.javaEnumSchemaFor.map[E](addDefault(default))
}

object ScalaEnumSchemaFor extends EnumSchemaFor {

  def apply[E <: scala.Enumeration#Value](default: E)(implicit tag: TypeTag[E]): SchemaFor[E] =
    SchemaFor.scalaEnumSchemaFor.map[E](addDefault(default))
}

object SchemaFor
    extends MagnoliaDerivedSchemaFors
    with ShapelessCoproductSchemaFors
    with CollectionAndContainerSchemaFors
    with TupleSchemaFors
    with ByteIterableSchemaFors
    with BaseSchemaFors {

  def apply[T](schema: Schema, fieldMapper: FieldMapper = DefaultFieldMapper) = {
    val s = schema
    val fm = fieldMapper
    new SchemaFor[T] {
      def schema: Schema = s
      def fieldMapper: FieldMapper = fm
    }
  }

  def apply[T](implicit schemaFor: SchemaFor[T]): SchemaFor[T] = schemaFor
}
